/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved.
 *
 */

import * as fse from 'fs-extra';

export interface LockOptions {
  stale?: number;
  update?: number;
  retries?: number;
}

export interface LockInfo {
  mtime?: number;
  lastUpdate?: number;
  option?: LockOptions;
  lockPath?: string;
  updateTimer?: NodeJS.Timer;
  release?: boolean;
}

const lockInfos = new Map<string, LockInfo>();

/**
 * 同步上锁，当文件夹存在并且没有超时时，认为已获得锁
 * @param {string} file
 * @param {LockOptions} opt
 */
export function lockSync(file: string, opt?: LockOptions) {
  const option: LockOptions = normalizeOption(opt);

  let retryTimes = 0;
  while (retryTimes <= option.retries!) {
    if (!checkSync(file)) {
      try {
        getLockSync(file, option);
        break;
      } catch (e: any) {
        // do nothing
      }
      retryTimes++;
    }
  }
  if (retryTimes >= option.retries!) {
    // 尝试100次获取文件锁后还是失败
    throw new Error(`The registration information of the daemon cannot be obtained. Delete ${file}.lock and try again`);
  }

  // 开始保持更新持有锁
  updateLock(file, option);
}

function getLockSync(file: string, option: LockOptions) {
  const lockFile = `${file}.lock`;
  try {
    // 尝试新建锁
    fse.mkdirSync(lockFile);
  } catch (e: any) {
    // 无法新建锁，如果原因不是目录已存在，抛出原生异常
    if (e.code !== 'EEXIST') {
      throw e;
    }

    // 检查锁是否已经超时，超时的锁可以重用，否则报错
    try {
      const stat = fse.statSync(lockFile);
      if (isLockStale(stat, option)) {
        fse.removeSync(lockFile);
        lockSync(file, option);
      } else {
        throw new Error(`Lock file ${lockFile} has been held by other process.`);
      }
    } catch (e: any) {
      // 如果目录又不存在了，重新尝试获取锁，因此而导致死循环的情况概率极低
      if (e.code === 'EONENT') {
        lockSync(file, option);
      } else {
        throw e;
      }
    }
  }

  // 如果mkdir没有错误，说明已经取得了锁
  const mtime = new Date(Date.now() + 5);
  try {
    // 尝试更新修改时间
    fse.utimesSync(lockFile, mtime, mtime);
  } catch (e: any) {
    // 无法更新时间，就移除锁
    fse.removeSync(lockFile);
    throw e;
  }
  lockInfos.set(file, {
    mtime: mtime.getTime(),
    lastUpdate: Date.now(),
    option: option,
    lockPath: lockFile,
    release: false,
  });
}

/**
 * 通过更新修改时间来更新锁
 * @param {string} file
 * @param {LockOptions} option
 */
function updateLock(file: string, option: LockOptions) {
  const lockFile = `${file}.lock`;
  const lockInfo = lockInfos.get(file);

  const removeLockInfo = () => {
    clearInterval(lockInfo?.updateTimer as unknown as number);
    lockInfos.delete(file);
    fse.removeSync(lockFile);
  };

  if (lockInfo === undefined) {
    return;
  }

  // 设置定时器来定时更新锁文件的修改时间
  lockInfo.updateTimer = setInterval(() => {
    // 当调用unlock方法时，将会将lockInfo.release置为true，定时器将会自己销毁
    if (lockInfo.release) {
      clearInterval(lockInfo.updateTimer as unknown as number);
      fse.removeSync(lockFile);
    } else {
      let isOverThreshold = lockInfo.lastUpdate! + option.stale! < Date.now();
      const updateTime = new Date(Date.now());

      // 如果与lockInfo内信息不匹配，说明该锁并不是由自己持有，终止定时器更新
      try {
        if (lockInfo.mtime !== fse.statSync(lockFile).mtime.getTime() || lockInfo.mtime === undefined) {
          removeLockInfo();
          return;
        }
      } catch (e: any) {
        // 锁文件不存在或已超时，终止定时器更新
        if (e.code === 'ENOENT' || isOverThreshold) {
          removeLockInfo();
        }
      }

      // 尝试更新锁的修改时间
      try {
        fse.utimesSync(lockFile, updateTime, updateTime);
      } catch (e: any) {
        // 锁文件不存在或已超时，终止定时器更新
        isOverThreshold = lockInfo.lastUpdate! + option.stale! < Date.now();
        if (e.code === 'ENOENT' || isOverThreshold) {
          removeLockInfo();
        }
      }

      // 更新内存中关于该锁的lockInfo
      lockInfo.mtime = updateTime.getTime();
      lockInfo.lastUpdate = Date.now();
    }
  }, option.update);

  lockInfo.updateTimer?.unref();
}

/**
 * 通过删除文件来解除文件锁
 * @param {string} file
 */
export function unlockSync(file: string) {
  const lockFile = `${file}.lock`;
  const lockInfo = lockInfos.get(file);
  if (lockInfo) {
    lockInfos.delete(file);
    lockInfo.release = true;
    clearInterval(lockInfo.updateTimer as unknown as number);
    fse.removeSync(lockFile);
  }
}

/**
 * 检查一个文件是否处于被锁定的状态
 * @param {string} file
 * @param {LockOptions} opt
 * @returns {boolean}
 */
export function checkSync(file: string, opt?: LockOptions) {
  const lockFile = `${file}.lock`;
  const option = normalizeOption(opt);
  if (fse.existsSync(lockFile)) {
    try {
      const stat = fse.statSync(lockFile);
      return !isLockStale(stat, option);
    } catch (e: any) {
      if (e.code === 'ENOENT') {
        return false;
      } else {
        throw e;
      }
    }
  }
  return false;
}

function normalizeOption(option?: LockOptions): LockOptions {
  const normalizeOption: LockOptions = {
    stale: 10000,
    retries: 0,
    update: 5000,
    ...option,
  };
  normalizeOption.stale = Math.max(normalizeOption.stale || 2000);
  normalizeOption.update = Math.max(normalizeOption.update!, normalizeOption.stale / 2);
  return normalizeOption;
}

function isLockStale(stat: fse.Stats, option: LockOptions) {
  return stat.mtime.getTime() + option.stale! < Date.now();
}
